﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ObjectGenerator.cs" company="CodePlex">
//   Copyright © CodePlex 2012
// </copyright>
// <summary>
//   Defines the ObjectGenerator type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace DataFrameworkGenerator
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Reflection;
    using System.Text;

    using DataFrameworkGenerator.Factories;
    using DataFrameworkGenerator.Models;

    /// <summary>
    /// Generates object to be used with the data framework.
    /// </summary>
    public class ObjectGenerator // http://allenbrowne.com/ser-49.html
    {
        /// <summary>
        /// The connection string used to talk to the database.
        /// </summary>
        private readonly string connectionString;

        /// <summary>
        /// The namespace for the common classes that we generate.
        /// </summary>
        private readonly string commonClassNamespace;

        /// <summary>
        /// The namespace for the dao classes that we generate.
        /// </summary>
        private readonly string daoClassNamespace;

        /// <summary>
        /// The namespace that the data framework resides in.
        /// </summary>
        private readonly string dataFrameworkNamespace;

        /// <summary>
        /// Directory where the generated classes will be output.
        /// </summary>
        private readonly string outputDirectory;

        /// <summary>
        /// The company name that will be inserted into file headers.
        /// </summary>
        private readonly string companyName;

        /// <summary>
        /// The current year inserted into the copyright.
        /// </summary>
        private readonly string currentYear = DateTime.Now.Year.ToString(CultureInfo.InvariantCulture);

        /// <summary>
        /// The type of database we are generating objects from.
        /// </summary>
        private readonly DatabaseType databaseType;

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectGenerator"/> class.
        /// </summary>
        /// <param name="databaseType">Type of the database.</param>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="commonClassNamespace">The class namespace.</param>
        /// <param name="daoClassNamespace">The DAO class namespace.</param>
        /// <param name="dataFrameworkNamespace">The data framework namespace.</param>
        /// <param name="companyName">Name of the company.</param>
        /// <param name="outputDirectory">The output directory.</param>
        public ObjectGenerator(
            DatabaseType databaseType,
            string connectionString, 
            string commonClassNamespace, 
            string daoClassNamespace, 
            string dataFrameworkNamespace, 
            string companyName,
            string outputDirectory)
        {
            this.databaseType = databaseType;
            this.connectionString = connectionString;
            this.commonClassNamespace = commonClassNamespace;
            this.daoClassNamespace = daoClassNamespace;
            this.dataFrameworkNamespace = dataFrameworkNamespace;
            this.companyName = companyName;
            this.outputDirectory = outputDirectory;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectGenerator"/> class.
        /// </summary>
        /// <param name="inputs">The inputs.</param>
        public ObjectGenerator(ObjectGeneratorInputs inputs)
        {
            this.databaseType = inputs.DatabaseType;
            this.connectionString = inputs.ConnectionString;
            this.commonClassNamespace = inputs.CommonClassNamespace;
            this.daoClassNamespace = inputs.DaoClassNamespace;
            this.dataFrameworkNamespace = inputs.DataFrameworkNamespace;
            this.companyName = inputs.CompanyName;
            this.outputDirectory = inputs.OutputDirectory;

            this.TargetSchema = inputs.DatabaseType == DatabaseType.Oracle ? inputs.TargetSchema : null;
        }

        /// <summary>
        /// Occurs when [progress update].
        /// </summary>
        public event EventHandler<ProgressEventArgs> ProgressUpdate;

        /// <summary>
        /// Gets or sets the target schema. This is currently only used against Oracle DBs.
        /// </summary>
        /// <value>The target schema.</value>
        public string TargetSchema { get; set; }

        /// <summary>
        /// Generates the classes.
        /// </summary>
        public void GenerateClasses()
        {
            var classes = this.GenerateCommonClasses();
            this.GenerateDaoClasses(classes);
        }

        /// <summary>
        /// Gets the class template.
        /// </summary>
        /// <returns>the class template</returns>
        private static string GetCommonClassTemplate()
        {
            var strm = Assembly.GetExecutingAssembly().GetManifestResourceStream("DataFrameworkGenerator.Templates.CommonClassTemplate.txt");
            if (strm == null)
            {
                throw new ArgumentException("Could not find the common class template.");
            }

            var streamReader = new StreamReader(strm);
            return streamReader.ReadToEnd();
        }

        /// <summary>
        /// Gets the property template.
        /// </summary>
        /// <returns>the property template</returns>
        private static string GetCommonPropertyTemplate()
        {
            var strm = Assembly.GetExecutingAssembly().GetManifestResourceStream("DataFrameworkGenerator.Templates.CommonPropertyTemplate.txt");
            if (strm == null)
            {
                throw new ArgumentException("Could not find the common property template.");
            }

            var streamReader = new StreamReader(strm);
            return streamReader.ReadToEnd();
        }

        /// <summary>
        /// Gets the DAO template.
        /// </summary>
        /// <returns>the property template</returns>
        private static string GetDaoClassTemplate()
        {
            var strm = Assembly.GetExecutingAssembly().GetManifestResourceStream("DataFrameworkGenerator.Templates.DaoClassTemplate.txt");
            if (strm == null)
            {
                throw new ArgumentException("Could not find the DAO class template.");
            }

            var streamReader = new StreamReader(strm);
            return streamReader.ReadToEnd();
        }

        /// <summary>
        /// Converts the name of the table name to class.
        /// </summary>
        /// <param name="tableName">Name of the table.</param>
        /// <returns>A properly formatted class name.</returns>
        private static string ConvertTableNameToClassName(string tableName)
        {
            /* Here we are attempting to take some common table name approaches and convert that 
             * table name to something that complies with standard practice in .Net. Ex. SOME_TABLES
             * would be converted to SomeTable. Also, tables like CONTACTS would get converted to 
             * Contact.
             */

            StringBuilder buffer;

            // Chop off the 's' on the first word. SOME_TABLES would yield SOME_TABLE. I'm a bit lazy and don't 
            // worry about "ies" or anything like that. FACTORIES would end up being Factorie.
            // I figure devs can check this and change it as needed.
            if (tableName.EndsWith("s") || tableName.EndsWith("S"))
            {
                buffer = new StringBuilder(tableName.Substring(0, tableName.Length - 1));
            }
            else
            {
                buffer = new StringBuilder(tableName);
            }

            // This is where we go through each "word" and lowercase letters if it's all upper. The
            // end result here would be converting SOME_TABLE to Some_Table.
            if (tableName.Contains("_") || tableName == tableName.ToUpperInvariant())
            {
                var isFirstLetter = true;
                for (int i = 0; i < buffer.Length; i++)
                {
                    var c = buffer[i];
                    if (char.IsUpper(c) && !isFirstLetter)
                    {
                        buffer[i] = char.ToLower(c);
                    }
                    else
                    {
                        isFirstLetter = false;
                    }

                    if (!char.IsLetterOrDigit(buffer[i]))
                    {
                        isFirstLetter = true;
                    }
                }

                // Finally we get rid of the underscores to go from Some_Table to SomeTable.
                buffer.Replace("_", string.Empty);
            }

            return buffer.ToString();
        }

        /// <summary>
        /// Converts the name of to C sharp friendly property.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="className">Name of the class.</param>
        /// <returns>a properly formatted c# property name.</returns>
        private static string ConvertToCSharpFriendlyPropertyName(string name, string className)
        {
            /* Here we try to convert common column names to something that adheres to standard
             * .Net practice. Column names like CONTACT_NAME would get converted to Name.
             */

            // Remove the class name.
            var buffer = name.StartsWith(className) ? new StringBuilder(name.Substring(className.Length)) : new StringBuilder(name);

            // This is where we go through each "word" and lowercase letters if it's all upper. The
            // end result here would be converting _NAME to _Name.
            if (name.Contains("_") || name == name.ToUpperInvariant())
            {
                var isFirstLetter = true;

                for (int i = 0; i < buffer.Length; i++)
                {
                    var c = buffer[i];
                    if (char.IsUpper(c) && !isFirstLetter)
                    {
                        buffer[i] = char.ToLower(c);
                    }
                    else
                    {
                        isFirstLetter = false;
                    }

                    if (!char.IsLetterOrDigit(buffer[i]))
                    {
                        isFirstLetter = true;
                    }
                }

                // Remove all those nasty _'s
                buffer.Replace("_", string.Empty);
            }

            return buffer.ToString();
        }

        /// <summary>
        /// Generates the common classes.
        /// </summary>
        /// <returns>Class names that were generated.</returns>
        private List<string> GenerateCommonClasses()
        {
            // Create all the variables that are needed to process.
            var classes = new List<string>();
            var conversionHelper = ConversionHelperFactory.FetchConversionHelper(this.databaseType);
            conversionHelper.TargetSchema = this.TargetSchema;
            var tables = conversionHelper.FetchDbTables(this.connectionString);
            var currentItem = 1;

            // Go through all the tables in the schema we are looking at and generate a common object for each of them.
            foreach (var tableName in tables)
            {
                var className = ConvertTableNameToClassName(tableName);

                var classBuffer = new StringBuilder(GetCommonClassTemplate());
                classBuffer.Replace("$NAMESPACE$", this.commonClassNamespace);
                classBuffer.Replace("$DATA_FRAMEWORK_NAMESPACE$", this.dataFrameworkNamespace);
                classBuffer.Replace("$COMPANY$", this.companyName);
                classBuffer.Replace("$YEAR$", this.currentYear);
                classBuffer.Replace("$CLASS_NAME$", className);
                classBuffer.Replace("$TABLE_NAME$", tableName);

                var columns = conversionHelper.FetchTableColumnMetadata(this.connectionString, tableName);
                var classBody = new StringBuilder();
                for (int i = 0; i < columns.Count; i++)
                {
                    var columnMetadata = columns[i];
                    var propertyBuffer = new StringBuilder(GetCommonPropertyTemplate());

                    propertyBuffer.Replace("$COLUMN_NAME$", columnMetadata.ColumnName);
                    propertyBuffer.Replace("$DBTYPE$", conversionHelper.ConvertDataTypeToDbDataType(columnMetadata));
                    propertyBuffer.Replace("$IS_PK$", columnMetadata.IsKeyField ? "true" : "false");
                    propertyBuffer.Replace("$DATA_TYPE$", conversionHelper.ConvertDataTypeToCodeType(columnMetadata));
                    propertyBuffer.Replace("$PROPERTY_NAME$", ConvertToCSharpFriendlyPropertyName(columnMetadata.ColumnName, className));

                    classBody.Append(propertyBuffer.ToString());

                    if (i + 1 < columns.Count)
                    {
                        classBody.Append(Environment.NewLine);
                        classBody.Append(Environment.NewLine);
                    }
                }

                classBuffer.Replace("$CLASS_BODY$", classBody.ToString());

                // Make sure a directory exists in our output path so we can seperate the common objects and daos.
                if (!Directory.Exists(Path.Combine(this.outputDirectory, string.Format("Common"))))
                {
                    Directory.CreateDirectory(Path.Combine(this.outputDirectory, string.Format("Common")));
                }

                // write the file out.
                File.WriteAllText(Path.Combine(this.outputDirectory, string.Format("Common\\{0}.cs", className)), classBuffer.ToString());
                classes.Add(className);

                // Let the UI know we've made progress!
                if (this.ProgressUpdate != null)
                {
                    // assume that for each common object generated a DAO will also be generated.
                    this.ProgressUpdate(this, new ProgressEventArgs { CurrentPosition = currentItem, TotalPositions = tables.Count * 2 });
                    currentItem++;
                }
            }

            return classes;
        }

        /// <summary>
        /// Generates the DAO classes.
        /// </summary>
        /// <param name="classes">The classes.</param>
        private void GenerateDaoClasses(ICollection<string> classes)
        {
            // Go through all the tables in the schema we are looking at and generate a common object for each of them.
            var currentItem = classes.Count + 1;
            foreach (var commonClass in classes)
            {
                var classBuffer = new StringBuilder(GetDaoClassTemplate());
                var className = string.Format("{0}Dao", commonClass);
                classBuffer.Replace("$NAMESPACE$", this.daoClassNamespace);
                classBuffer.Replace("$DATA_FRAMEWORK_NAMESPACE$", this.dataFrameworkNamespace);
                classBuffer.Replace("$COMMON_NAMESPACE$", this.commonClassNamespace);
                classBuffer.Replace("$COMPANY$", this.companyName);
                classBuffer.Replace("$YEAR$", this.currentYear);
                classBuffer.Replace("$COMMON_CLASS_NAME$", commonClass);
                classBuffer.Replace("$CLASS_NAME$", className);

                // Make sure a directory exists in our output path so we can seperate the common objects and daos.
                if (!Directory.Exists(Path.Combine(this.outputDirectory, string.Format("DAOs"))))
                {
                    Directory.CreateDirectory(Path.Combine(this.outputDirectory, string.Format("DAOs")));
                }

                // write the file out.
                File.WriteAllText(Path.Combine(this.outputDirectory, string.Format("DAOs\\{0}.cs", className)), classBuffer.ToString());

                if (this.ProgressUpdate != null)
                {
                    // assume that for each DAO that must be generated there was also a common object generated.
                    this.ProgressUpdate(this, new ProgressEventArgs { CurrentPosition = currentItem, TotalPositions = classes.Count * 2 });
                    currentItem++;
                }
            }
        }
    }
}